class Snowflake {
  private twepoch: number;
  private dataCenterIdBits: number;
  private workerIdBits: number;
  private sequenceBits: number;
  private maxWorkerId: number;
  private maxDataCenterId: number;
  private sequenceMask: number;
  private workerIdShift: number;
  private dataCenterIdShift: number;
  private timestampLeftShift: number;
  private lastTimestamp: number;
  private sequence: number;
  private workerId: number;
  private dataCenterId: number;

  constructor(
    workerId: number,
    dataCenterId: number,
    twepoch: number = 1288834974657,
  ) {
    this.twepoch = twepoch;
    this.dataCenterIdBits = 5;
    this.workerIdBits = 5;
    this.sequenceBits = 12;

    this.maxWorkerId = -1 ^ (-1 << this.workerIdBits);
    this.maxDataCenterId = -1 ^ (-1 << this.dataCenterIdBits);

    if (workerId > this.maxWorkerId || workerId < 0) {
      throw new Error(
        `worker Id can't be greater than ${this.maxWorkerId} or less than 0`,
      );
    }

    if (dataCenterId > this.maxDataCenterId || dataCenterId < 0) {
      throw new Error(
        `data center Id can't be greater than ${this.maxDataCenterId} or less than 0`,
      );
    }

    this.sequenceMask = -1 ^ (-1 << this.sequenceBits);

    this.workerIdShift = this.sequenceBits;
    this.dataCenterIdShift = this.sequenceBits + this.workerIdBits;
    this.timestampLeftShift =
      this.sequenceBits + this.workerIdBits + this.dataCenterIdBits;

    this.lastTimestamp = -1;
    this.sequence = 0;

    this.workerId = workerId;
    this.dataCenterId = dataCenterId;
  }

  private timeGen(): number {
    return Date.now();
  }

  private tilNextMillis(lastTimestamp: number): number {
    let timestamp = this.timeGen();
    while (timestamp <= lastTimestamp) {
      timestamp = this.timeGen();
    }
    return timestamp;
  }

  private getTimestamp(): number {
    return this.timeGen() - this.twepoch;
  }

  public nextId(): number {
    let timestamp = this.getTimestamp();

    if (timestamp < this.lastTimestamp) {
      throw new Error(
        `Clock moved backwards. Refusing to generate id for ${this.lastTimestamp - timestamp} milliseconds`,
      );
    }

    if (this.lastTimestamp === timestamp) {
      this.sequence = (this.sequence + 1) & this.sequenceMask;
      if (this.sequence === 0) {
        timestamp = this.tilNextMillis(this.lastTimestamp);
      }
    } else {
      this.sequence = 0;
    }

    this.lastTimestamp = timestamp;

    const id =
      ((timestamp << this.timestampLeftShift) |
        (this.dataCenterId << this.dataCenterIdShift) |
        (this.workerId << this.workerIdShift) |
        this.sequence) >>>
      0;

    return id;
  }
}

export const snowflake = new Snowflake(1, 1);
